5.4.3 方法集

方法集定义了接口的接受规则。看一下代码清单 5-36 所示的代码,有助于理解方法集在接口中的重要角色。

代码清单5-36 listing36.go

01 // 这个示例程序展示Go语言里如何使用接口
02 package main
03
04 import (
05   "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16   name string
17   email string
18 }
19
20 // notify是使用指针接收者实现的方法
21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }
26
27 // main是应用程序的入口
28 func main() {
29   // 创建一个user类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }
39
40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

代码清单5-36中的程序虽然看起来没问题,但实际上却无法通过编译。在第10行中,声明了一个名为 notifier 的接口,包含一个名为 notify 的方法。第15行中,声明了名为 user 的实体类型,并通过第21行中的方法声明实现了 notifier 接口。这个方法是使用 user 类型的指针接收者实现的。

代码清单5-37 listing36.go:第40行到第44行

40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

在代码清单5-37的第42行,声明了一个名为 sendNotification 的函数。这个函数接收一个 notifier 接口类型的值。之后,使用这个接口值来调用 notify 方法。任何一个实现了 notifier 接口的值都可以传入 sendNotification 函数。现在让我们来看一下 main 函数,如代码清单5-38所示。

代码清单5-38 listing36.go:第28行到第38行

28 func main() {
29   // 创建一个user类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }

main 函数里,代码清单5-38的第30行,创建了一个 user 实体类型的值,并将其赋值给变量u。之后在第32行将u的值传入 sendNotification 函数。不过,调用 sendNotification 的结果是产生了一个编译错误,如代码清单5-39所示。

代码清单5-39 将 user 类型的值存入接口值时产生的编译错误

./listing36.go:32: 不能将u(类型是user)作为sendNotification的参数类型notifier:
 user类型并没有实现notifier(notify方法使用指针接收者声明)

既然 user 类型已经在第21行实现了 notify 方法,为什么这里还是产生了编译错误呢?让我们再来看一下那段代码,如代码清单5-40所示。

代码清单5-40 listing36.go:第08行到第12行,第21行到第25行

08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }
21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }

代码清单5-40展示了接口是如何实现的,而编译器告诉我们 user 类型的值并没有实现这个接口。如果仔细看一下编译器输出的消息,其实编译器已经说明了原因,如代码清单 5-41所示。

代码清单5-41 进一步查看编译器错误

(notify method has pointer receiver)

要了解用指针接收者来实现接口时为什么 user 类型的值无法实现该接口,需要先了解 方法集 。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

让我们先解释一下Go语言规范里定义的方法集的规则,如代码清单5-42所示。

代码清单5-42 规范里描述的方法集

Values        Methods Receivers
-----------------------------------------------
  T          (t T)
  *T          (t T) and (t *T)

代码清单5-42展示了规范里对方法集的描述。描述中说到, T 类型的值的方法集只包含值接收者声明的方法。而指向 T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。从值的角度看这些规则,会显得很复杂。让我们从接收者的角度来看一下这些规则,如代码清单5-43所示。

代码清单5-43 从接收者类型的角度来看方法集

Methods Receivers   Values 
-----------------------------------------------
  (t T)         T and *T
  (t *T)         *T

代码清单5-43展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。现在再看一下代码清单5-36所示的代码,就能理解出现编译错误的原因了,如代码清单5-44所示。

代码清单5-44 listing36.go:第28行到第38行

28 func main() {
29   // 使用user类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }

我们使用指针接收者实现了接口,但是试图将 user 类型的值传给 sendNotification 方法。代码清单5-44的第30行和第32行清晰地展示了这个问题。但是,如果传递的是 user 值的地址,整个程序就能通过编译,并且能够工作了,如代码清单5-45所示。

代码清单5-45 listing36.go:第28行到第35行

28 func main() {
29   // 使用user类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(&u)
33
34   // 传入地址,不再有错误
35 }

在代码清单5-45里,这个程序终于可以编译并且运行。因为使用指针接收者实现的接口,只有 user 类型的指针可以传给 sendNotification 函数。

现在的问题是,为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址,如代码清单5-46所示。

代码清单5-46 listing46.go

01 // 这个示例程序展示不是总能
02 // 获取值的地址
03 package main
04
05 import "fmt"
06
07 // duration是一个基于int类型的类型
08 type duration int
09
10 // 使用更可读的方式格式化duration值
11 func (d *duration) pretty() string {
12   return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main是应用程序的入口
16 func main() {
17   duration(42).pretty()
18
19   // ./listing46.go:17: 不能通过指针调用duration(42)的方法
20   // ./listing46.go:17: 不能获取duration(42)的地址
21 }

代码清单5-46所示的代码试图获取duration类型的值的地址,但是获取不到。这展示了不能总是获得值的地址的一种情况。让我们再看一下方法集的规则,如代码清单5-47所示。

代码清单5-47 再看一下方法集的规则

Values        Methods Receivers
-----------------------------------------------
  T          (t T)
  *T          (t T) and (t *T)
 Methods Receivers   Values
-----------------------------------------------
  (t T)         T and *T
  (t *T)        *T

因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。

results matching ""

    No results matching ""